Skip to content

The add-on partitioned once, custom multigrid on empty ranks, and the rotated solve's switch and record - #890

Merged
lmoresi merged 67 commits into
developmentfrom
feature/fault-split-integration
Oct 29, 2026
Merged

lmoresi merged 67 commits into
developmentfrom
feature/fault-split-integration

Conversation

@lmoresi

@lmoresi lmoresi commented Oct 29, 2026

Copy link
Copy Markdown
Member

The rest of the fault-split work, stacked on #889 (split-level fault multigrid); retarget to development when that merges. Together with #882 (the rebuild leak) this is everything UWTN 2026-018 and 2026-022 describe.

What it adds

  • The add-on partitioned once (utilities/addon_partition.py, docs/developer/design/parallel-addon-distribution-2026-10.md): the finest mesh built on a static base by adapt, a cut, a placement or a split is partitioned for load once, with its add-on levels, and every transfer to it is partition-agnostic (recorded provenance, not location). A second partition keeps a split fault whole; faults are glued by name. Reviewed 2026-10-17 (record in docs/reviews/2026-10/).
  • Custom multigrid on ranks with no coarse cells (bfae3e6 and its parent): the custom-P hierarchy and the cut survive a rank that holds none of a coarse level (the kaiju np 32/52 death).
  • The rotated solve's Newton switch measured against the same reference as its convergence test (a warm start no longer stays in Picard to the end), and its record: the residual history and the line-search steps, read by the moving-fault drivers.
  • A thin sheet's prescribed normal velocity (ThinSheet.normal_velocity): the breathing of a tidally flexed shell.
  • The plex topology tables in Cython (_plex_topology.pyx).

Measured

UWTN 2026-022's fixture on this head: split 5 / welded 13 velocity iterations at k = 3, peak slip 0.1589860839 / 0.1589859512 (the note's figures); the note's "Using it" recipe runs as written. On the head before today's merges, the note's README numbers reproduced at np 1 (all of them) and np 4 (same hierarchies; iterations within one; slip within 2.4e-6).

Review

The parallel-addon lineage was reviewed on 2026-10-17. Today a pass on the whole of this PR's delta (the merged rotated solve, today's merges, the add-on and empty-rank code, the tests) found two must-fix items and several should-fix ones; the first comment lists them and what was done.

Suites (laptop, macOS, this head): tier A level 1 serial 1498 passed, 44 skipped, 0 failed; tests/parallel at np 2 180 passed, 15 skipped, 1 failed; at np 4 191 passed, 4 skipped, 1 failed; no hangs. The one failure is test_1063_constrained_freeslip_parallel[ti] (#826), as on development. Until #882 merges this PR's diff includes it; it drops out when #882 lands on development and #889 takes development in.

Issues

Left open by this PR, found in its review: #883 (the cold-start datum lift's convergence reference), #884 (the rotation-gauge projection measured from the second assembly), #885 (custom_mg.last_builder_stats is module-global; curved boundaries trigger the cross-partition rebuild), #886 (a rank-local exception decides build_transfers' retry), #887 (docs and messages behind the code), #888 (test reach: the cross-partition builder, an np 2 empty-rank case, test_1094's clamp).

Underworld development team with AI support from Claude Code

…a datum-only solve

ThinSheet.normal_velocity sets the datum of the sheet-tangency constraint, so a
sheet on a manifold can move along its normal (the radial motion of a tidally
flexed shell over an ocean) and its in-plane strain rate carries the curvature
term through the nodal normals. In the rotated nonlinear loop a cold start whose
only forcing is the prescribed datum had a zero residual at u = 0 and returned
converged in zero iterations before the lift was applied; the convergence test
now waits for the lift and takes its reference scale from the lifted iterate.

tests/test_1092: breathing (u = W rhat exactly, stress 6 eta W P) and the
degree-2 membrane closed form (tangential velocity 3/11 of the radial pattern's
surface gradient; 7.2, 3.4, 2.5 percent at h 0.3, 0.2, 0.15).

scripts/sessions/tidal_shell/tidal_shell.py: the eccentricity-tide driver
(degree 2, viscous or Maxwell sheet, collective diagnostics against the closed
form, Mollweide maps).

Underworld development team with AI support from Claude Code
… sphere, in-plane principal stresses

The maps were drawn from a rank-local evaluation of the whole grid (garbage outside rank 0's cells) and the largest principal stress picked up the zero normal eigenvalue where both in-plane stresses were compressive.

Underworld development team with AI support from Claude Code
… datum on fault pairs

When rigid rotations are a genuine null space (a drag-free sheet on a sphere, a
free-slip shell) the inner solve cannot reduce the residual's component along
them, so the outer nonlinear residual is now measured in that quotient space,
as it already was for the pressure gauge. A viscoelastic stress history on the
faceted surface leaves such a component (measured: a tidally flexed Maxwell
sheet floored at a relative residual of 1e-4 with every linear solve converged;
one iteration per step after the change). The projected-out magnitude is kept
on the solver as _rotated_rotation_gauge_residual.

A split-fault pair on a manifold takes its tangency through the pair block, so a
prescribed sheet normal velocity now rides the pair's mean-normal row (sqrt2 w,
signed against the analytic normal); before, the fault nodes stood still while
the sheet breathed (datum error 0.018 against an amplitude of 0.1).

Tidal driver: tiger-stripe faults (count, length, spacing, latitude, interface
law), per-step stress diagnostics on a collective grid through nodal stress
components (the stress expression's velocity gradients cannot cross ranks),
the objective-rate option, and the sweep plot script.

Underworld development team with AI support from Claude Code
…, shear invariant, speed) and the faulted-versus-unfaulted comparison script

Underworld development team with AI support from Claude Code
…re stress against a reference run

Underworld development team with AI support from Claude Code
…circle optimum) beside the stripe-parallel receivers

Underworld development team with AI support from Claude Code
…rden, driving-traction probes on virtual stripes, stripe azimuth, cycle plots and the Berne et al. figure analogue

CoulombFaultLaw takes a constant effective normal stress added to the reaction-fed one (compression positive), the clamped-but-modulated fault of Berne et al. 2024.

Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
…ckness to the sheet's resultant units; clamping reported as a stress

Underworld development team with AI support from Claude Code
…et topology

On a sheet on a sphere the fault is an edge carried by a 3-vector; the
de-smearing mass picked the triangle's weights by the component count and gave
the mid-edge nodes an eighth of their mass, so the effective normal stress the
interface laws read (and the friction they produced) was off by node-dependent
factors of 5 to 8. The reaction is the membrane resultant, L sigma_nn, now to
0.4 percent on every pair of the breathing sphere (test_1092).

Underworld development team with AI support from Claude Code
…he prediction in the sweep plot

Amplitude (24/11) mu A wt/sqrt(1+wt^2) and lag atan(1/wt): measured at 128 steps
per period 0.0195 against 0.0193 and 32 against 28 degrees at De 0.3; the lag
excess is first order in the step (15 degrees at 32 steps per period).

Underworld development team with AI support from Claude Code
…tion coefficient

Underworld development team with AI support from Claude Code
… saved per step for the site analysis

Underworld development team with AI support from Claude Code
…pe-plane tractions (phase of the clamping, Coulomb asymmetry index); the overlay takes labelled runs

Underworld development team with AI support from Claude Code
… azimuth

Underworld development team with AI support from Claude Code
…ycle, the excess and the slip it drives

Underworld development team with AI support from Claude Code
…and polar grids, saved at cycle boundaries

Underworld development team with AI support from Claude Code
…ulted

Underworld development team with AI support from Claude Code
…rules

A fault placed as a ladder on a copy of the base's finest level gets only the
welded tail (A); in a yielding, convecting box its velocity sub-solve caps at
200 once the lid mobilises. Building C by hand, the same ladder at 4S and 2S
on the coarser base levels, each split along its own rungs, keeps the
Newton-phase velocity iterations at 29-38 with no caps (A: 71-200, 2 caps)
at A's cost per cycle.

New capability guide guides/faults-in-convection.md (skill link included):
the lean tail, the rotated loop's warm-start rules (first solve from rest;
newton_switch_rtol measured from each solve's own residual, so warm solves
need it at 1), rebuilding for a moving trench (shifted warm start, Picard
first, rebuild every few rungs), and the measured traps. The design note
records the A / C comparison and lists the ribbon replay as not yet done.

Underworld development team with AI support from Claude Code
Claude-Session: https://claude.ai/code/session_019aromUkeWWPzcL2Kb8365E
…le-integrated activity and net offset), saved at orbit boundaries

Underworld development team with AI support from Claude Code
… profiles (Berne et al. Fig. 4 style)

Underworld development team with AI support from Claude Code
… kinked chain of arcs lost part of its cut silently)

Underworld development team with AI support from Claude Code
…polar view

Underworld development team with AI support from Claude Code
…pe block

Underworld development team with AI support from Claude Code
…noise floor (the unfaulted shell's residual)

Underworld development team with AI support from Claude Code
…before the cycle's first step (it missed one step)

Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
Three fixes found by the tumbling-fault Mohr sweep, brought onto this branch
for the box fault study:

- fault_contact: the 3-D fault normal traction is recovered pointwise (a
  linear fit of the P2-triangle consistent load on the facet vertices) rather
  than by dividing by P1 sub-lumped masses, which read zero at every vertex and
  4/3 of the value at every midpoint. The branch is keyed on the facet
  topology (mesh.dim), not the coordinate dimension, so the edge fault on a
  surface mesh keeps its lumped path.
- place_surface: the sheet cavity is closed in the three shapes that defeated
  it on adapted meshes (an enclosed kept cell, a pinched shell vertex, a flat
  fill tet), with a bounded refill loop.
- fault_split: a single split_fault keeps the mesh's multigrid tail, as the
  network path already did.

Tests: the pointwise 3-D traction and the multigrid-tail checks in
test_0848; the adapted-mesh placement cases as test_0867 (0864 was taken
on this branch).

Underworld development team with AI support from Claude Code
…s glued by name

Two blocking findings. A cut, placed or split child of a partitioned host could
not be partitioned again (the fault-network route places one sheet at a time),
because the first add-on level's parent on the base was searched locally on a
pair the first partition had moved apart; it is now read from the recorded
provenance. And a split fault's facets are single-sided, so a support-based
interior test never glued it and a second partition could peel its star across
a seam; the faults a mesh carries are now glued by name (every pairing and its
sides, every placed sheet), which also stops a domain-spanning internal boundary
from being glued.

The provenance is recorded when the surgery makes a mesh (inherit_provenance in
the cut, the placement and the split), so it survives a later move of the nodes
and the transfer needs no location. The provenance pair keeps the finest
level's FAC patch. Empty region ids no longer reach the balance; a level that
would sit on one rank is refused; the inside tolerances agree; the polyline
reach is reconciled over the seam; the balance cannot overshoot by a unit; the
remaining non-collective raises are collective; the 3-D sheet fault on a refined
base takes its coarse levels in serial as in parallel.

Tests: a second partition keeps the first fault whole (2-D, np 2/4/8); two
sheets placed in turn and split together (3-D); the placement test compares
facets and pairs with its own serial run and the tail by identity; fixtures
shared in tests/parallel/fault_fixtures.py. Docs: the design note's unit and
retirement statements corrected, supersession notes in the September placement
note, FAULT_MULTIGRID_HIERARCHIES and the conforming-surfaces page; review record
in docs/reviews/2026-10.

Underworld development team with AI support from Claude Code
…tes partitioned

utilities/_plex_topology.pyx builds, in one C loop per stratum, the tables the
fault, placement and add-on code assembled one petsc4py call at a time:
closures by depth, supports, a per-cell section and its offsets, each cell's
dof rows. The partition, the region growth shared with the placement gather,
the marking rules, the redistribution's per-cell fields, the edge and cell-size
tables, the mesh's point-location index and per-cell node table are now array
operations on them, and the node-cloud transfer builders assemble once instead
of a row per call. Every converted function is identical to its previous
version, entry for entry, serially and on four ranks. The 3-D partition at
87,525 cells on four ranks went from 6.87 s to 1.35 s, the 2-D one at 94,910
from 2.94 s to 0.64 s.

The point-location index takes each cell's vertex count from the closure: the
element table gives 4 for a 1-D surface mesh's line cell, and the old slice of
the closure took the edge point itself as a vertex there. The same count in
three other mesh functions is flagged TODO(BUG).

FaultNetwork.build (3-D band) and place_fault_ribbon_2d in gather mode
partition before placing (mark_band, a superset of the placement's mask), so
the placement moves nothing and adopt_hierarchy keeps the base's own levels
under the partitioned host instead of co-locating copies. A direct place_sheet
call gathers with two growth layers; with one it refused on a partitioned
adapt child.

A pre-existing mismatch is flagged TODO(BUG): the native checkpoint payload is
written under /restart/petsc while the split-mesh reader looks at the top
level, so test_0864's two-sided round trip fails here and on feature/tidal-shell.

Tests: test_1098 (tables against DMPlex, serial and 3 ranks), test_1099 (band
route, 2 and 4 ranks), test_1097 direct placement (shown failing first).
Quick tier A: 1484 passed, 1 failed (the flagged checkpoint test); parallel
batch at 2 ranks all passed.

Underworld development team with AI support from Claude Code
The table that says how many faces and vertices a cell has had no 1-D entry,
so a 1-D surface mesh (an annulus boundary, extract_surface) fell through to
the tetrahedron entry and read 4 faces and 4 vertices per line cell. The face
control-point builders, the geometry audit and the visualisation slice cell
closures by those counts, and a line's closure has 3 points, so the slice took
the line itself as a vertex. A line cell now has two faces, its two vertices,
and the three TODO(BUG) markers left by the previous commit are resolved at
the source rather than per function.

Point location on a 1-manifold now evaluates exactly: the annulus case of
test_0772's surface evaluate, an expected failure that blamed exactly this,
passes serially and on 2 and 3 ranks, and is no longer marked.

Test: tests/test_1100_element_table_matches_closures.py (the table against
every cell's cone and closure on a 1-D surface, triangles, quadrilaterals,
tetrahedra and a 2-D surface in 3-D), shown failing on the line case first.
Every serial and parallel test building a 1-D or manifold mesh passes.

Underworld development team with AI support from Claude Code
A coarse level is small, and a placed fault mesh is gathered onto its
surgery rank, so on a large run most ranks own no cell of the coarsest
level and many own no fine cell either. Found on kaiju at np 104 with a
124-cell coarsest level (the faulted annulus): ranks without cells raised,
the others waited in the next all-gather. Four places assumed a rank holds
something:

- _colocate_level shaped its centroid arrays from the cells it found. An
  empty rank gave shape (0,), so np.zeros((0, shape[1])) raised IndexError
  and the vstack of the all-gathered centroids failed on every rank. Both
  arrays are now (-1, cdim) from the DM's coordinate dimension.
- build() took each level's component count as local DOFs over local
  nodes: ZeroDivisionError on a rank with no node. It is the field's count,
  now taken over the whole level.
- _node_transfer_no_repair triangulated the rank's local coarse cloud:
  "No points given" on an empty rank. A rank with no fine node returns no
  rows; one with too few coarse nodes to span a simplex counts its fine
  nodes as outside the local hull, which already sends the "auto" build
  across partitions.
- _fac_patch_split_parallel indexed the coarse selection with a clipped
  index on rows that have no coarse node: IndexError on a rank with fine
  rows and no coarse nodes (seen at np 16).

tests/parallel/test_1103 (written first): colocating a 4-cell coarsest
level with empty ranks, and a custom-P FMG Poisson solve on a 4-cell
coarsest level against GAMG. Before: IndexError / ZeroDivisionError on 7
of 8 ranks and a hang on the eighth; after the first three fixes,
IndexError at np 16. Now 2 passed at np 4, 5, 8, 16 and 32. The parallel
custom-MG tests (1017, 1064, 1095, 1096, 1097, 1099): 22 passed, 3
skipped at np 2 and 4. tests/parallel/test_*.py runs it in CI.

Underworld development team with AI support from Claude Code
Two more places that assumed every rank holds part of a level, both on the
first faulted-mesh build:

- line_cut._edge_vertices built its (n_edges, 2) table from the edges it
  found; a rank with none got shape (0,), and _crossing_parameters' ends[:, 0]
  raised IndexError there while the other ranks waited. It is now
  reshape(-1, 2).
- fault_mg.provenance_transfer_parallel packed its star-forest roots with
  reshape(nc_loc, -1); with no coarse cell, reshape(0, -1) cannot infer the
  width and raised ValueError. The widths are now explicit. The raise was
  caught by build_transfers' per-rank retry, so the failing ranks started an
  RBF rebuild alone and the job died in mismatched collectives
  (MPI_ERR_BUFFER, "Message truncated") -- the kaiju np 32 / 52 signature.
  Measured on a faulted annulus at np 32 (cell 0.35, four refinements) and
  np 8 (cell 0.8): the three or two ranks with no coarse cell raised here.

tests/parallel/test_1103 gains a line cut on a mesh where most ranks hold no
cell (written first: IndexError on 7 of 8 ranks and a hang on the eighth;
now 3 passed at np 8). The provenance fix has no CI-sized test yet: the
faulted annulus at np 8 still diverges later, inside the rotated solve, and
is kept out of the suite until that is understood. Provenance-path tests
(1096, 1097, 1099) at np 2: 9 passed.

Underworld development team with AI support from Claude Code
…eference

The rotated nonlinear loop tests convergence against ref = max(r0,
||F(0)||), the rest-state residual (the warm-start rtol cure), but its
Picard->Newton continuation switch still read rnorm <= switch_rtol * r0,
the warm start's own residual. A warm start is already in Newton's basin,
yet had to shrink its residual a hundredfold under Picard first: on the
fault campaign, rebuild steps took 23-27 nonlinear iterations against 4
for ordinary steps. The switch now reads ref, and the result dict records
the iteration it fired at (continuation_switch_iteration).

Measured on test_1018's power-law box, re-solved from a converged state
after a 1% change in the body force: switch at iteration 12, 13 iterations
in all, before; switch at iteration 0, 2 iterations, after. The cold solve
is unchanged (switch at 15, 17 iterations): there ref is r0.

The generic path was already consistent: its continuation ramp keys alpha
and the stop on F_ref = max(||F(0)||, F_start). No other solver loop
compares a threshold with r0.

test_rotated_continuation_warm_start_switches_to_newton_at_once (written
first; with only the record added it failed "assert 12 in (0, 1)").
Rotated suite (test_1018 x2, 0062, 1066, 1021, 1058, 0203, 0019, 0020,
0013, 1070, 1073): 133 passed.

Underworld development team with AI support from Claude Code
… reference

A campaign that wants to record how its nonlinear solves behave (the zg95
annulus rollback runs, where fault rebuilds are the point of the experiment)
had to scrape -uw_verbose logs for numbers the loop already computes. The
result dict now carries them:

- "fnorm_rest": the rest-state residual ||F^(0)|| (warm starts; None cold)
- "ref": max(r0, fnorm_rest), what the convergence and Picard->Newton
  switch tests compare against
- "rnorm_history": ||F^|| at every iteration start (its or its+1 entries;
  the last equals "rnorm")
- "line_search_alpha": the step accepted for each increment, one per
  increment (1.0 full step or the lift; a halved alpha on a cutback; 0.0
  when the increment was not applied: a line-search stall or the step-norm
  exit)

_backtracking_line_search returns (u, improved, alpha); its only caller is
the rotated loop. No extra residual evaluations or solves.

test_rotated_solve_records_its_diagnostics (written first; on 371d3bd it
failed with KeyError 'rnorm_history'): cold and warm re-solves of the
power-law box, checking the lengths, the history's ends against rnorm0 and
rnorm, fnorm_rest None cold and > 0 warm, ref = max(rnorm0, fnorm_rest), and
a full final step at the solution. test_1018: 27 passed (kaiju, 1 rank,
overlay on 371d3bd).

Written on kaiju (local commit 16ae9d2b) and relayed through the hub, since
kaiju holds no GitHub credentials.

Underworld development team with AI support from Claude Code
…eference

The rotated nonlinear loop tests convergence against ref = max(r0,
||F(0)||), the rest-state residual (the warm-start rtol cure), but its
Picard->Newton continuation switch still read rnorm <= switch_rtol * r0,
the warm start's own residual. A warm start is already in Newton's basin,
yet had to shrink its residual a hundredfold under Picard first: on the
fault campaign, rebuild steps took 23-27 nonlinear iterations against 4
for ordinary steps. The switch now reads ref, and the result dict records
the iteration it fired at (continuation_switch_iteration).

Measured on test_1018's power-law box, re-solved from a converged state
after a 1% change in the body force: switch at iteration 12, 13 iterations
in all, before; switch at iteration 0, 2 iterations, after. The cold solve
is unchanged (switch at 15, 17 iterations): there ref is r0.

The generic path was already consistent: its continuation ramp keys alpha
and the stop on F_ref = max(||F(0)||, F_start). No other solver loop
compares a threshold with r0.

test_rotated_continuation_warm_start_switches_to_newton_at_once (written
first; with only the record added it failed "assert 12 in (0, 1)").
Rotated suite (test_1018 x2, 0062, 1066, 1021, 1058, 0203, 0019, 0020,
0013, 1070, 1073): 133 passed.

Underworld development team with AI support from Claude Code
… reference

A campaign that wants to record how its nonlinear solves behave (the zg95
annulus rollback runs, where fault rebuilds are the point of the experiment)
had to scrape -uw_verbose logs for numbers the loop already computes. The
result dict now carries them:

- "fnorm_rest": the rest-state residual ||F^(0)|| (warm starts; None cold)
- "ref": max(r0, fnorm_rest), what the convergence and Picard->Newton
  switch tests compare against
- "rnorm_history": ||F^|| at every iteration start (its or its+1 entries;
  the last equals "rnorm")
- "line_search_alpha": the step accepted for each increment, one per
  increment (1.0 full step or the lift; a halved alpha on a cutback; 0.0
  when the increment was not applied: a line-search stall or the step-norm
  exit)

_backtracking_line_search returns (u, improved, alpha); its only caller is
the rotated loop. No extra residual evaluations or solves.

test_rotated_solve_records_its_diagnostics (written first; on 371d3bd it
failed with KeyError 'rnorm_history'): cold and warm re-solves of the
power-law box, checking the lengths, the history's ends against rnorm0 and
rnorm, fnorm_rest None cold and > 0 warm, ref = max(rnorm0, fnorm_rest), and
a full final step at the solution. test_1018: 27 passed (kaiju, 1 rank,
overlay on 371d3bd).

Written on kaiju (local commit 16ae9d2b) and relayed through the hub, since
kaiju holds no GitHub credentials.

Underworld development team with AI support from Claude Code
# Conflicts:
#	src/underworld3/utilities/rotated_bc.py
#	tests/test_1018_rotated_freeslip.py
…' into HEAD

# Conflicts:
#	src/underworld3/utilities/rotated_bc.py
… the review fixes)

Six conflicts, resolved:
- constitutive_models.py (two hunks) and ddt.py (two): development's
  _etd_maxwell_time / _update_exp_values (#851: the step as its own constant,
  the declared vanishing floor, expm1, units) supersede this lineage's
  9e97670, which made the same repair (a viscosity varying in space keeps
  its elastic response); test_1093 from 9e97670 stays and checks it.
- solvers.py: development's #850 guard in _check_velocity_preconditioner, with
  this lineage's fault-contact marker kept (a faulted solve with no walls takes
  the same rotated loop).
- thin_sheet.py: the lineage's normal_velocity property, then the review's
  add_basal_drag(conds, ...).
- scripts/test.sh: the review's comment on the test_108*/test_109* batch.
- fault-split-multigrid.md: the lineage's text (it removed co-location: the
  add-on is partitioned once), not the review's co-location bullet.

Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code

# Conflicts:
#	scripts/sessions/fault_mg/fig_parallel_load.py
…gauge vectors

- guides/faults-in-convection.md (a capability guide, read as a skill) said
  solve(picard=N) raises on the rotated path and that the Newton switch reads
  the warm start's own residual, with newton_switch_rtol = 1.0 as the
  workaround; on this branch picard=N stages Picard then Newton and the switch
  reads the convergence reference (371d3bd). Corrected.
- Campaign drivers and probes leave the repository (Louis, 2026-10-28):
  scripts/sessions/parallel_addon, tidal_shell, box_fault_cyclic, and the last
  fault_mg file; the design note's probe references say where they live.
- test_1093 duplicated development's test_1053 (same fixture and checks) and
  is removed. New tests start at tier B: test_1098, test_1100's table test,
  and the two newest tests of test_1018, whose tiers now go on each test.
- The rotated solve destroys its rotation-gauge mode vectors (one leaked per
  mode per solve) and warns when the projected-out rigid-rotation component
  dominates a converged residual, as the pressure gauge does (a net torque on
  free-slip walls has no solution and reported converged in silence).
- FAULT_MULTIGRID_HIERARCHIES no longer lists co-location as to come.

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Oct 29, 2026

Copy link
Copy Markdown
Member Author

Adversarial review

The parallel-addon lineage had its own review on 2026-10-17 (docs/reviews/2026-10/). This pass (2026-10-29, read-only) covered the whole of this PR's delta: the merged rotated_bc.py (two lineages edited the same hunk of solve_rotated_freeslip), today's merges of #889 and #882 into this branch, the add-on and empty-rank code where the earlier review did not reach, and the tests. Every item below is fixed in this branch unless it names an issue.

Fixed

  • guides/faults-in-convection.md (a capability guide, read by sessions as a skill) said solve(picard=N) raises on the rotated path and that the Newton switch reads the warm start's own residual, with newton_switch_rtol = 1.0 as the workaround. On this branch picard=N stages Picard then Newton (test_1018::test_rotated_freeslip_picard_under_newton_stages_picard_then_newton) and the switch reads the convergence reference (371d3bd). Corrected.
  • Campaign drivers and probes (scripts/sessions/parallel_addon, tidal_shell, box_fault_cyclic, a fault_mg figure script) left the branch (Louis, 2026-10-15 and 2026-10-28); the design note says where the probes live.
  • The rotation-gauge projection leaked one Vec per mode per solve, and a forcing with a net torque on free-slip walls (no solution) reported converged in silence; the modes are destroyed, and a dominant projected-out component warns, as the pressure gauge does.
  • test_1093 duplicated development's test_1053 (same fixture and checks); removed.
  • New tests started at tier A (test_1098, test_1100's table test, two new tests in test_1018 inheriting its module tier); tier B, and test_1018's tiers go on each test.
  • FAULT_MULTIGRID_HIERARCHIES.md listed co-location as to come; it was done and retired.

Merge resolutions (c842c11)

Left open (filed)

Attacks that failed

  • Co-location is gone: colocate_split_level, fault_star_owner and _fault_star_cells have no reference in src or tests.
  • normal_velocity touches neither Split-level fault multigrid, the thin-sheet solver, and sheet extrusion #889's constitutive_model setter nor the pressure refresh; a datum forfeits the cross-solve cache, so a changed datum cannot ride a stale Q. On fault pairs the datum row matches the stored factor.
  • Parallel uniformity of the rotated loop: every new branch reads collective norms or dots; _fitted_normal_traction reaches _collective_raise once on every rank, early return included.
  • The info keys the note's recipe and the tests read (vel_its_last, pres_its_last, converged, velocity_pc) are present; rnorm_history and line_search_alpha have consistent lengths on all three increment branches.
  • _plex_topology.pyx: the extern PetscInt is the header's type; empty strata reshape cleanly; the kd-tree index order matches the old closure slice.
  • redistribute_by_assignment: all three callers take the new 4-tuple.
  • CI reach: check_test_coverage.py reports every test file reachable; fault_fixtures.py is not collected.

Merge conditions

@github-actions

Copy link
Copy Markdown

🔍 Architectural Review Checklist

Thank you for submitting an architectural review! Reviewers should validate:

Design & Architecture

  • Design rationale is clear and well-justified
  • Trade-offs are documented with alternatives considered
  • System architecture is comprehensible
  • Integration points are clearly identified

Implementation

  • Implementation matches documented design
  • Code quality meets project standards
  • Breaking changes are identified and justified
  • Backward compatibility is properly addressed

Testing & Validation

  • Testing strategy is adequate for the changes
  • Test coverage is sufficient
  • Edge cases are properly covered
  • Performance impact has been assessed

Documentation

  • Known limitations are clearly documented
  • Benefits are quantified with metrics
  • User-facing changes are documented
  • Migration guide provided (if needed)

Review Process: See CODE-REVIEW-PROCESS.md

Approval: This PR merge = Review formally approved

@github-actions

Copy link
Copy Markdown

✅ Test Suite: success

…base level

The prose placed the 2S and 4S ladders one and two levels below the finest.
The guide's own code (host = base_levels[len - k] for k = chain..1) and its
cell counts (lean chain 642, 2924, 10950, 11612) put 2S on the finest base
level beside the S ladder and 4S one level down; the coarser levels stay
welded.

Underworld development team with AI support from Claude Code
@github-actions

Copy link
Copy Markdown

✅ Test Suite: success

@lmoresi
lmoresi changed the base branch from feature/fault-split-multigrid to development October 29, 2026 20:19
@lmoresi
lmoresi merged commit fc67b65 into development Oct 29, 2026
4 checks passed
lmoresi added a commit that referenced this pull request Oct 29, 2026
Development now carries #863's sharded CI, and #882, #889 and #890 (this
branch's base). The one conflict was scripts/test.sh: test_1090's serial
entry now takes the batch form, on shard 4 beside test_1069. All 382 test
files are reachable (check_test_coverage).

Underworld development team with AI support from Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant